/** * Grid block tool for Editor.js. * Allows users to create responsive layouts with any number of columns and choose different content types, manual widths, * and dynamically instantiates other registered editor plugins inside columns. */ class GridTool { static get toolbox() { return { title: 'Grid / Columns', icon: '' }; } constructor({ data, api, readOnly }) { this.api = api; this.readOnly = readOnly; this.data = { cols: parseInt(data.cols) || 2, globalClass: data.globalClass || '', globalId: data.globalId || '', globalAttributes: data.globalAttributes || data.attributes || '' }; this.activeInstances = {}; // Load column contents dynamically for (let i = 1; i <= 12; i++) { this.data[`type${i}`] = data[`type${i}`] || 'html'; this.data[`col${i}`] = data[`col${i}`] || ''; this.data[`imgUrl${i}`] = data[`imgUrl${i}`] || ''; this.data[`ytUrl${i}`] = data[`ytUrl${i}`] || ''; this.data[`accTitle${i}`] = data[`accTitle${i}`] || ''; this.data[`accContent${i}`] = data[`accContent${i}`] || ''; this.data[`class${i}`] = data[`class${i}`] || ''; this.data[`id${i}`] = data[`id${i}`] || ''; this.data[`attr${i}`] = data[`attr${i}`] || data[`attributes${i}`] || ''; this.data[`width${i}`] = parseInt(data[`width${i}`]) || 0; // 0 means auto this.data[`caption${i}`] = data[`caption${i}`] || ''; // Sub-plugin properties this.data[`subType${i}`] = data[`subType${i}`] || 'none'; this.data[`colsub_${i}`] = data[`colsub_${i}`] || ''; this.data[`imgUrlsub_${i}`] = data[`imgUrlsub_${i}`] || ''; this.data[`ytUrlsub_${i}`] = data[`ytUrlsub_${i}`] || ''; this.data[`accTitlesub_${i}`] = data[`accTitlesub_${i}`] || ''; this.data[`accContentsub_${i}`] = data[`accContentsub_${i}`] || ''; this.data[`classsub_${i}`] = data[`classsub_${i}`] || ''; this.data[`idsub_${i}`] = data[`idsub_${i}`] || ''; this.data[`attrsub_${i}`] = data[`attrsub_${i}`] || ''; } this.wrapper = undefined; } render() { this.wrapper = document.createElement('div'); this.wrapper.className = 'p-3 bg-light border rounded mb-3 ce-grid-tool-wrapper'; this.wrapper.style.fontFamily = 'inherit'; const title = document.createElement('div'); title.className = 'font-weight-bold text-primary small mb-3'; title.innerHTML = ' Multi-Column Grid Layout'; this.wrapper.appendChild(title); // Layout Settings Row const settingsRow = document.createElement('div'); settingsRow.className = 'form-row mb-3 pb-2 border-bottom'; // 1. Columns const colDiv = document.createElement('div'); colDiv.className = 'col-md-2 mb-2'; colDiv.innerHTML = ''; const colInput = document.createElement('input'); colInput.type = 'number'; colInput.className = 'form-control form-control-sm'; colInput.min = '1'; colInput.max = '12'; colInput.value = this.data.cols; if (this.readOnly) colInput.disabled = true; colInput.addEventListener('input', (e) => { let val = parseInt(e.target.value) || 2; if (val < 1) val = 1; if (val > 12) val = 12; this.data.cols = val; this._renderColumnInputs(); }); colDiv.appendChild(colInput); settingsRow.appendChild(colDiv); // 2. Global CSS Class const classDiv = document.createElement('div'); classDiv.className = 'col-md-3 mb-2'; classDiv.innerHTML = ''; const classInput = document.createElement('input'); classInput.type = 'text'; classInput.className = 'form-control form-control-sm'; classInput.placeholder = 'e.g. my-custom-grid'; classInput.value = this.data.globalClass || ''; if (this.readOnly) classInput.disabled = true; classInput.addEventListener('input', (e) => this.data.globalClass = e.target.value.trim()); this.globalClassInput = classInput; classDiv.appendChild(classInput); settingsRow.appendChild(classDiv); // 3. Global HTML ID const idDiv = document.createElement('div'); idDiv.className = 'col-md-3 mb-2'; idDiv.innerHTML = ''; const idInput = document.createElement('input'); idInput.type = 'text'; idInput.className = 'form-control form-control-sm'; idInput.placeholder = 'e.g. grid-section-1'; idInput.value = this.data.globalId || ''; if (this.readOnly) idInput.disabled = true; idInput.addEventListener('input', (e) => this.data.globalId = e.target.value.trim()); this.globalIdInput = idInput; idDiv.appendChild(idInput); settingsRow.appendChild(idDiv); // 4. Global Custom Attributes const attrDiv = document.createElement('div'); attrDiv.className = 'col-md-4 mb-2'; attrDiv.innerHTML = ''; const attrInput = document.createElement('input'); attrInput.type = 'text'; attrInput.className = 'form-control form-control-sm'; attrInput.placeholder = 'e.g. data-aos="fade-up" style="background:#fff"'; attrInput.value = this.data.globalAttributes || ''; if (this.readOnly) attrInput.disabled = true; attrInput.addEventListener('input', (e) => this.data.globalAttributes = e.target.value.trim()); this.globalAttrInput = attrInput; attrDiv.appendChild(attrInput); settingsRow.appendChild(attrDiv); this.wrapper.appendChild(settingsRow); // Columns inputs container this.inputsContainer = document.createElement('div'); this.wrapper.appendChild(this.inputsContainer); this._renderColumnInputs(); return this.wrapper; } _renderColumnInputs() { this.inputsContainer.innerHTML = ''; const row = document.createElement('div'); row.className = 'row d-flex flex-wrap'; const count = this.data.cols; const colWidthClass = count === 1 ? 'col-12' : (count === 2 ? 'col-md-6' : (count === 3 ? 'col-md-4' : (count === 4 ? 'col-md-3' : 'col-md'))); for (let i = 1; i <= count; i++) { const colDiv = document.createElement('div'); colDiv.className = `${colWidthClass} mb-3 p-2 border rounded bg-white`; colDiv.style.boxShadow = '0 1px 3px rgba(0,0,0,0.05)'; const headerRow = document.createElement('div'); headerRow.className = 'd-flex justify-content-between align-items-center mb-2 pb-1 border-bottom'; const label = document.createElement('label'); label.className = 'small font-weight-bold text-dark mb-0'; label.innerText = `Col ${i}`; headerRow.appendChild(label); const controlsWrapper = document.createElement('div'); controlsWrapper.className = 'd-flex align-items-center gap-1'; // Width Selector const widthSelect = document.createElement('select'); widthSelect.className = 'custom-select custom-select-sm mr-1'; widthSelect.style.width = '75px'; if (this.readOnly) widthSelect.disabled = true; const widthOpts = [ { val: 0, lbl: 'Auto' }, { val: 1, lbl: '1/12' }, { val: 2, lbl: '2/12' }, { val: 3, lbl: '3/12' }, { val: 4, lbl: '4/12' }, { val: 5, lbl: '5/12' }, { val: 6, lbl: '6/12' }, { val: 7, lbl: '7/12' }, { val: 8, lbl: '8/12' }, { val: 9, lbl: '9/12' }, { val: 10, lbl: '10/12' }, { val: 11, lbl: '11/12' }, { val: 12, lbl: '12/12' } ]; widthOpts.forEach(o => { const opt = document.createElement('option'); opt.value = o.val; opt.innerText = o.lbl; opt.selected = this.data[`width${i}`] === o.val; widthSelect.appendChild(opt); }); widthSelect.addEventListener('change', (e) => { this.data[`width${i}`] = parseInt(e.target.value) || 0; }); controlsWrapper.appendChild(widthSelect); // Column Type Select (Built-ins + Registered plugins dynamically) const typeSelect = document.createElement('select'); typeSelect.className = 'custom-select custom-select-sm'; typeSelect.style.width = '100px'; if (this.readOnly) typeSelect.disabled = true; // Base types const types = [ { value: 'html', label: 'HTML/Text' }, { value: 'image', label: 'Image' }, { value: 'youtube', label: 'YouTube' }, { value: 'accordion', label: 'Accordion' } ]; // Dynamically add other registered plugins from window.SISEditorPlugins if (window.SISEditorPlugins) { Object.keys(window.SISEditorPlugins).forEach(key => { // Allow nesting any registered plugins (no exclusions) if (!types.some(t => t.value === key)) { types.push({ value: key, label: `[Plugin] ${key}` }); } }); } types.forEach(t => { const opt = document.createElement('option'); opt.value = t.value; opt.innerText = t.label; opt.selected = this.data[`type${i}`] === t.value; typeSelect.appendChild(opt); }); const contentDiv = document.createElement('div'); typeSelect.addEventListener('change', (e) => { this.data[`type${i}`] = e.target.value; this._renderTypeSpecificInput(i, contentDiv); }); controlsWrapper.appendChild(typeSelect); headerRow.appendChild(controlsWrapper); colDiv.appendChild(headerRow); colDiv.appendChild(contentDiv); // --- Sub-Plugin (Sub-Content) Section --- const subWrapper = document.createElement('div'); if (this.data[`subType${i}`] !== 'none') { subWrapper.className = 'mt-3 pt-2 border-top'; } const subHeader = document.createElement('div'); subHeader.className = 'd-flex justify-content-between align-items-center mb-2'; const subLabel = document.createElement('label'); subLabel.className = 'small font-weight-bold text-muted mb-0'; subLabel.style.fontSize = '10px'; subLabel.innerText = 'SUB-CONTENT / INJECT PLUGIN'; subHeader.appendChild(subLabel); const subTypeSelect = document.createElement('select'); subTypeSelect.className = 'custom-select custom-select-sm'; subTypeSelect.style.width = '130px'; subTypeSelect.style.fontSize = '10px'; if (this.readOnly) subTypeSelect.disabled = true; const subTypes = [ { value: 'none', label: 'None' }, { value: 'caption', label: 'Text Caption' }, { value: 'html', label: 'HTML/Text' }, { value: 'image', label: 'Image' }, { value: 'youtube', label: 'YouTube' }, { value: 'accordion', label: 'Accordion' } ]; if (window.SISEditorPlugins) { Object.keys(window.SISEditorPlugins).forEach(key => { if (!subTypes.some(t => t.value === key)) { subTypes.push({ value: key, label: `[Plugin] ${key}` }); } }); } subTypes.forEach(t => { const opt = document.createElement('option'); opt.value = t.value; opt.innerText = t.label; opt.selected = this.data[`subType${i}`] === t.value; subTypeSelect.appendChild(opt); }); subHeader.appendChild(subTypeSelect); subWrapper.appendChild(subHeader); const subContentDiv = document.createElement('div'); subWrapper.appendChild(subContentDiv); colDiv.appendChild(subWrapper); const renderSubInputs = () => { subContentDiv.innerHTML = ''; const subType = this.data[`subType${i}`]; if (subType === 'none') { subWrapper.className = ''; } else { subWrapper.className = 'mt-3 pt-2 border-top'; } if (subType === 'none') { // Empty } else if (subType === 'caption') { const captionGroup = document.createElement('div'); captionGroup.className = 'form-group mb-2'; const captionInput = document.createElement('input'); captionInput.type = 'text'; captionInput.className = 'form-control form-control-sm'; captionInput.style.fontSize = '11px'; captionInput.placeholder = 'Optional column caption / text...'; captionInput.value = this.data[`caption${i}`] || ''; if (this.readOnly) captionInput.disabled = true; captionInput.addEventListener('input', (e) => { this.data[`caption${i}`] = e.target.value; }); captionGroup.appendChild(captionInput); subContentDiv.appendChild(captionGroup); } else { this._renderTypeSpecificInput(`sub_${i}`, subContentDiv, subType); } }; subTypeSelect.addEventListener('change', (e) => { this.data[`subType${i}`] = e.target.value; renderSubInputs(); }); row.appendChild(colDiv); this._renderTypeSpecificInput(i, contentDiv); renderSubInputs(); } this.inputsContainer.appendChild(row); } _renderTypeSpecificInput(i, container, explicitType) { container.innerHTML = ''; // Remove tracking of previous instance if (this.activeInstances[i]) { delete this.activeInstances[i]; } const typeContainer = document.createElement('div'); typeContainer.className = 'mb-3'; container.appendChild(typeContainer); const type = explicitType || this.data[`type${i}`] || 'html'; // Check if type is a dynamic editor plugin registered in window.SISEditorPlugins if (window.SISEditorPlugins && window.SISEditorPlugins[type]) { try { const pluginEntry = window.SISEditorPlugins[type]; const pluginClass = pluginEntry.class; const pluginConfig = pluginEntry.config || {}; let parsedData = {}; if (this.data[`col${i}`]) { try { parsedData = typeof this.data[`col${i}`] === 'string' ? JSON.parse(this.data[`col${i}`]) : this.data[`col${i}`]; } catch (e) { parsedData = { html: this.data[`col${i}`] }; // fallback } } const instance = new pluginClass({ data: parsedData, api: this.api, readOnly: this.readOnly, config: pluginConfig, block: { id: 'nested-grid-' + i + '-' + Math.random().toString(36).substring(7), name: type, holder: typeContainer, isEmpty: false, selected: false, stretched: false, tunes: {} } }); this.activeInstances[i] = instance; const element = instance.render(); typeContainer.appendChild(element); // Fix for Editor.js Image plugin 404 errors (spinner hangs forever) if (type === 'image') { // Custom UI override for nested images const injectCustomButtons = () => { if (element.querySelector('.sis-custom-image-controls')) return; const fileBtn = element.querySelector('.cdx-button'); if (fileBtn) { fileBtn.style.display = 'none'; // Hide native button } const controls = document.createElement('div'); controls.className = 'sis-custom-image-controls mt-2 p-2 border rounded bg-light'; controls.style.display = 'flex'; controls.style.flexDirection = 'column'; controls.style.gap = '8px'; controls.innerHTML = `
Image Controls
`; const uploadBtn = controls.querySelector(`#btn-upload-img-${i}`); if (uploadBtn && fileBtn) { uploadBtn.addEventListener('click', (e) => { e.preventDefault(); fileBtn.click(); }); } const urlBtn = controls.querySelector(`#btn-fetch-url-${i}`); if (urlBtn) { urlBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const url = prompt('Enter Image URL:'); if (url && url.trim() !== '') { this.data[`col${i}`] = { file: { url: url.trim() }, caption: '', withBorder: false, withBackground: false, stretched: false }; this._renderTypeSpecificInput(i, container); } }); } const mediaBtn = controls.querySelector(`#btn-media-lib-${i}`); if (mediaBtn) { mediaBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); if (window.SISMediaPicker) { SISMediaPicker.open((url, mediaObj, config) => { if (url) { this.data[`col${i}`] = { file: { url: url }, caption: (config && config.alt) || '', withBorder: false, withBackground: false, stretched: false }; if (config) { this.data[`imgStyle${i}`] = config.style || ''; this.data[`imgClass${i}`] = config.cssClass || ''; this.data[`imgAlt${i}`] = config.alt || ''; this.data[`imgAttrs${i}`] = config.customAttributes || ''; this.data[`imgAspectRatio${i}`] = config.aspectRatio || ''; } this._renderTypeSpecificInput(i, container); } }); } else { alert('Media Library is not yet configured on this system.'); } }); } const clearBtn = controls.querySelector(`#btn-clear-img-${i}`); if (clearBtn) { clearBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); if (confirm('Are you sure you want to remove this image?')) { this.data[`col${i}`] = ''; this._renderTypeSpecificInput(i, container); } }); } element.appendChild(controls); }; injectCustomButtons(); const handleImageError = () => { element.classList.remove('image-tool--loading'); element.classList.add('image-tool--empty'); const preloader = element.querySelector('.image-tool__image-preloader'); if (preloader) preloader.style.display = 'none'; }; const observer = new MutationObserver((mutations) => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.tagName === 'IMG') { node.addEventListener('error', handleImageError); } }); }); }); observer.observe(element, { childList: true, subtree: true }); const existingImg = element.querySelector('img'); if (existingImg) existingImg.addEventListener('error', handleImageError); } } catch (err) { console.error(`Failed to render plugin ${type} inside GridTool:`, err); typeContainer.innerHTML = `
Error loading plugin ${type}
`; } } else if (type === 'html') { const textarea = document.createElement('textarea'); textarea.className = 'form-control form-control-sm'; textarea.style.minHeight = '140px'; textarea.style.fontSize = '12px'; textarea.placeholder = `HTML / Text for Column ${i}...`; textarea.value = typeof this.data[`col${i}`] === 'object' ? JSON.stringify(this.data[`col${i}`]) : this.data[`col${i}`] || ''; if (this.readOnly) textarea.disabled = true; textarea.addEventListener('input', (e) => { this.data[`col${i}`] = e.target.value; }); typeContainer.appendChild(textarea); } else if (type === 'image') { const inputGroup = document.createElement('div'); inputGroup.className = 'input-group input-group-sm mb-2'; const input = document.createElement('input'); input.type = 'text'; input.className = 'form-control form-control-sm'; input.placeholder = 'Paste image URL here...'; input.value = this.data[`imgUrl${i}`] || ''; if (this.readOnly) input.disabled = true; const appendDiv = document.createElement('div'); appendDiv.className = 'input-group-append'; const chooseBtn = document.createElement('button'); chooseBtn.type = 'button'; chooseBtn.className = 'btn btn-outline-secondary'; chooseBtn.innerText = 'Choose'; if (this.readOnly) chooseBtn.disabled = true; chooseBtn.addEventListener('click', () => { if (window.SISMediaPicker) { SISMediaPicker.open((selectedUrl, mediaObj, config) => { input.value = selectedUrl; this.data[`imgUrl${i}`] = selectedUrl; if (config) { this.data[`imgStyle${i}`] = config.style || ''; this.data[`imgClass${i}`] = config.cssClass || ''; this.data[`imgAlt${i}`] = config.alt || ''; this.data[`imgAttrs${i}`] = config.customAttributes || ''; this.data[`imgAspectRatio${i}`] = config.aspectRatio || ''; } previewImg.src = selectedUrl; previewBox.style.display = 'block'; }); } else { alert('Media Picker modal helper is not loaded.'); } }); const uploadBtn = document.createElement('button'); uploadBtn.type = 'button'; uploadBtn.className = 'btn btn-outline-primary'; uploadBtn.innerText = 'Upload'; if (this.readOnly) uploadBtn.disabled = true; const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'image/*'; fileInput.style.display = 'none'; uploadBtn.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', async (e) => { const file = e.target.files[0]; if (!file) return; const formData = new FormData(); formData.append('image', file); uploadBtn.disabled = true; uploadBtn.innerText = '...'; try { const response = await fetch('/api/manage/media/upload', { method: 'POST', body: formData }); const result = await response.json(); if (result.success && result.file && result.file.url) { const url = result.file.url; input.value = url; this.data[`imgUrl${i}`] = url; previewImg.src = url; previewBox.style.display = 'block'; } else { alert('Upload failed: ' + (result.message || 'Unknown error')); } } catch (err) { console.error(err); alert('Upload error occurred.'); } finally { uploadBtn.disabled = false; uploadBtn.innerText = 'Upload'; } }); appendDiv.appendChild(chooseBtn); appendDiv.appendChild(uploadBtn); inputGroup.appendChild(input); inputGroup.appendChild(appendDiv); typeContainer.appendChild(inputGroup); typeContainer.appendChild(fileInput); const previewBox = document.createElement('div'); previewBox.className = 'border rounded p-1 text-center bg-light'; previewBox.style.display = this.data[`imgUrl${i}`] ? 'block' : 'none'; previewBox.style.maxHeight = '150px'; previewBox.style.overflow = 'hidden'; const previewImg = document.createElement('img'); previewImg.src = this.data[`imgUrl${i}`] || ''; previewImg.style.maxWidth = '100%'; previewImg.style.maxHeight = '140px'; previewImg.style.objectFit = 'contain'; previewBox.appendChild(previewImg); typeContainer.appendChild(previewBox); input.addEventListener('input', (e) => { const val = e.target.value.trim(); this.data[`imgUrl${i}`] = val; if (val) { previewImg.src = val; previewBox.style.display = 'block'; } else { previewBox.style.display = 'none'; } }); } else if (type === 'youtube') { const input = document.createElement('input'); input.type = 'text'; input.className = 'form-control form-control-sm mb-2'; input.placeholder = 'Paste YouTube video URL...'; input.value = this.data[`ytUrl${i}`] || ''; if (this.readOnly) input.disabled = true; const preview = document.createElement('div'); preview.className = 'small text-muted p-2 bg-light border rounded'; preview.innerText = 'Format: https://www.youtube.com/watch?v=XXXXXX'; input.addEventListener('input', (e) => { this.data[`ytUrl${i}`] = e.target.value.trim(); }); typeContainer.appendChild(input); typeContainer.appendChild(preview); } else if (type === 'accordion') { const titleInput = document.createElement('input'); titleInput.type = 'text'; titleInput.className = 'form-control form-control-sm mb-2'; titleInput.placeholder = 'Accordion Title...'; titleInput.value = this.data[`accTitle${i}`] || ''; if (this.readOnly) titleInput.disabled = true; const contentTextarea = document.createElement('textarea'); contentTextarea.className = 'form-control form-control-sm'; contentTextarea.style.minHeight = '90px'; contentTextarea.style.fontSize = '12px'; contentTextarea.placeholder = 'Accordion Body HTML...'; contentTextarea.value = this.data[`accContent${i}`] || ''; if (this.readOnly) contentTextarea.disabled = true; titleInput.addEventListener('input', (e) => { this.data[`accTitle${i}`] = e.target.value; }); contentTextarea.addEventListener('input', (e) => { this.data[`accContent${i}`] = e.target.value; }); typeContainer.appendChild(titleInput); typeContainer.appendChild(contentTextarea); } // Advanced Settings (ID and Class) const advancedHeader = document.createElement('div'); advancedHeader.className = 'font-weight-bold text-muted small mb-2'; advancedHeader.style.fontSize = '10px'; advancedHeader.innerText = 'ADVANCED SETTINGS'; container.appendChild(advancedHeader); const advRow = document.createElement('div'); advRow.className = 'form-row'; const classCol = document.createElement('div'); classCol.className = 'col'; const classInput = document.createElement('input'); classInput.type = 'text'; classInput.className = 'form-control form-control-sm'; classInput.style.fontSize = '11px'; classInput.placeholder = 'CSS Class(es)...'; classInput.value = this.data[`class${i}`] || ''; if (this.readOnly) classInput.disabled = true; classInput.addEventListener('input', (e) => { this.data[`class${i}`] = e.target.value.trim(); }); classCol.appendChild(classInput); const idCol = document.createElement('div'); idCol.className = 'col'; const idInput = document.createElement('input'); idInput.type = 'text'; idInput.className = 'form-control form-control-sm'; idInput.style.fontSize = '11px'; idInput.placeholder = 'HTML ID...'; idInput.value = this.data[`id${i}`] || ''; if (this.readOnly) idInput.disabled = true; idInput.addEventListener('input', (e) => { this.data[`id${i}`] = e.target.value.trim(); }); idCol.appendChild(idInput); advRow.appendChild(classCol); advRow.appendChild(idCol); container.appendChild(advRow); } save(blockContent) { const savedData = { cols: this.data.cols, globalClass: this.globalClassInput ? this.globalClassInput.value.trim() : (this.data.globalClass || ''), globalId: this.globalIdInput ? this.globalIdInput.value.trim() : (this.data.globalId || ''), globalAttributes: this.globalAttrInput ? this.globalAttrInput.value.trim() : (this.data.globalAttributes || '') }; for (let i = 1; i <= this.data.cols; i++) { const type = this.data[`type${i}`] || 'html'; savedData[`type${i}`] = type; // If it is a dynamically nested plugin, save its output data if (this.activeInstances[i]) { try { const pluginData = this.activeInstances[i].save(); savedData[`col${i}`] = pluginData; } catch (e) { console.error(`Failed to save plugin ${type} inside GridTool:`, e); savedData[`col${i}`] = this.data[`col${i}`]; } } else { savedData[`col${i}`] = this.data[`col${i}`] || ''; } savedData[`imgUrl${i}`] = this.data[`imgUrl${i}`] || ''; savedData[`ytUrl${i}`] = this.data[`ytUrl${i}`] || ''; savedData[`accTitle${i}`] = this.data[`accTitle${i}`] || ''; savedData[`accContent${i}`] = this.data[`accContent${i}`] || ''; savedData[`class${i}`] = this.data[`class${i}`] || ''; savedData[`id${i}`] = this.data[`id${i}`] || ''; savedData[`attr${i}`] = this.data[`attr${i}`] || ''; savedData[`width${i}`] = this.data[`width${i}`] || 0; savedData[`caption${i}`] = this.data[`caption${i}`] || ''; // Sub-plugin serialization const subType = this.data[`subType${i}`] || 'none'; savedData[`subType${i}`] = subType; if (this.activeInstances[`sub_${i}`]) { try { const subPluginData = this.activeInstances[`sub_${i}`].save(); savedData[`colsub_${i}`] = subPluginData; } catch (e) { console.error(`Failed to save sub-plugin ${subType} inside GridTool:`, e); savedData[`colsub_${i}`] = this.data[`colsub_${i}`]; } } else { savedData[`colsub_${i}`] = this.data[`colsub_${i}`] || ''; } savedData[`imgUrlsub_${i}`] = this.data[`imgUrlsub_${i}`] || ''; savedData[`ytUrlsub_${i}`] = this.data[`ytUrlsub_${i}`] || ''; savedData[`accTitlesub_${i}`] = this.data[`accTitlesub_${i}`] || ''; savedData[`accContentsub_${i}`] = this.data[`accContentsub_${i}`] || ''; savedData[`classsub_${i}`] = this.data[`classsub_${i}`] || ''; savedData[`idsub_${i}`] = this.data[`idsub_${i}`] || ''; savedData[`attrsub_${i}`] = this.data[`attrsub_${i}`] || ''; } return savedData; } } // Register the plugin globally window.SISEditorPlugins = window.SISEditorPlugins || {}; window.SISEditorPlugins['grid'] = { class: GridTool };